﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace ZCustomControl.Controls
{
    /// <summary>
    /// 按照步骤 1a 或 1b 操作，然后执行步骤 2 以在 XAML 文件中使用此自定义控件。
    ///
    /// 步骤 1a) 在当前项目中存在的 XAML 文件中使用该自定义控件。
    /// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根 
    /// 元素中: 
    ///
    ///     xmlns:MyNamespace="clr-namespace:ZCustomControl.Controls"
    ///
    ///
    /// 步骤 1b) 在其他项目中存在的 XAML 文件中使用该自定义控件。
    /// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根 
    /// 元素中: 
    ///
    ///     xmlns:MyNamespace="clr-namespace:ZCustomControl.Controls;assembly=ZCustomControl.Controls"
    ///
    /// 您还需要添加一个从 XAML 文件所在的项目到此项目的项目引用，
    /// 并重新生成以避免编译错误: 
    ///
    ///     在解决方案资源管理器中右击目标项目，然后依次单击
    ///     “添加引用”->“项目”->[浏览查找并选择此项目]
    ///
    ///
    /// 步骤 2)
    /// 继续操作并在 XAML 文件中使用控件。
    ///
    ///     <MyNamespace:Waiting/>
    ///
    /// </summary>
    public class Waiting : Control
    {
        #region Fields

        private double[] _cosValueArray = null;
        private double[] _sinValueArray = null;

        private Canvas _canvas = null;
        private List<Shape> _dotList = null;

        private DispatcherTimer _animationTimer = null;
        private int _firstDotPositionIndex = 0;

        #endregion

        #region Properties

        #endregion

        #region Dependency Properties

        public static readonly DependencyProperty IsBusyProperty
            = DependencyProperty.Register("IsBusy", typeof(bool), typeof(Waiting), new PropertyMetadata(false, OnIsBusyPropertyChangedCallback));
        public static readonly DependencyProperty BusyTipTextProperty
            = DependencyProperty.Register("BusyTipText", typeof(string), typeof(Waiting), new PropertyMetadata("业务繁忙，请等待..."));
        public static readonly DependencyProperty RegionCountProperty
            = DependencyProperty.Register("RegionCount", typeof(int), typeof(Waiting), new PropertyMetadata(16));
        public static readonly DependencyProperty DotCountProperty
            = DependencyProperty.Register("DotCount", typeof(int), typeof(Waiting), new PropertyMetadata(9));
        public static readonly DependencyProperty DotForegroundProperty
            = DependencyProperty.Register("DotForeground", typeof(Brush), typeof(Waiting), new PropertyMetadata(new SolidColorBrush(Colors.White)));
        public static readonly DependencyProperty DotRadiusProperty
            = DependencyProperty.Register("DotRadius", typeof(double), typeof(Waiting), new PropertyMetadata(6.0, OnDotRadiusPropertyChangedCallback));
        public static readonly DependencyProperty CircleRadiusProperty
            = DependencyProperty.Register("CircleRadius", typeof(double), typeof(Waiting), new PropertyMetadata(60.0, OnCircleRadiusPropertyChangedCallback));

        #endregion

        #region Dependency Property Wrappers

        public bool IsBusy
        {
            get { return (bool)GetValue(IsBusyProperty); }
            set { SetValue(IsBusyProperty, value); }
        }

        public string BusyTipText
        {
            get { return (string)GetValue(BusyTipTextProperty); }
            set { SetValue(BusyTipTextProperty, value); }
        }

        public int RegionCount
        {
            get { return (int)GetValue(RegionCountProperty); }
            set { SetValue(RegionCountProperty, value); }
        }

        public int DotCount
        {
            get { return (int)GetValue(DotCountProperty); }
            set { SetValue(DotCountProperty, value); }
        }

        public Brush DotForeground
        {
            get { return (Brush)GetValue(DotForegroundProperty); }
            set { SetValue(DotForegroundProperty, value); }
        }

        public double DotRadius
        {
            get { return (double)GetValue(DotRadiusProperty); }
            set { SetValue(DotRadiusProperty, value); }
        }

        public double CircleRadius
        {
            get { return (double)GetValue(CircleRadiusProperty); }
            set { SetValue(CircleRadiusProperty, value); }
        }

        #endregion

        #region Routed Events

        #endregion

        #region Routed Event Wrappers

        #endregion

        #region Constructors

        static Waiting()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Waiting), new FrameworkPropertyMetadata(typeof(Waiting)));
        }

        #endregion

        #region Override Methods

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _canvas = GetTemplateChild("PART_Canvas") as Canvas;
            if (_canvas != null)
            {
                UpdateCanvasSize();

                _canvas.Loaded += canvas_Loaded;
                _canvas.Unloaded += canvas_Unloaded;
            }

            InitRegionPositionFactors();
            InitAnimationTimer();
            InitDots();

            if (IsBusy)
            {
                Start();
            }
            else
            {
                Visibility = Visibility.Collapsed;
            }
        }

        #endregion

        #region Property Changed Callbacks

        private static void OnIsBusyPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Waiting loading = d as Waiting;
            if (loading == null)
            {
                return;
            }

            bool isBusy = (bool)e.NewValue;

            if (isBusy)
            {
                loading.Visibility = Visibility.Visible;
                loading.Start();
            }
            else
            {
                loading.Visibility = Visibility.Collapsed;
                loading.Stop();
            }
        }

        private static void OnDotRadiusPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Waiting waiting = d as Waiting;
            if (waiting == null || waiting._canvas == null)
            {
                return;
            }

            waiting.UpdateCanvasSize();
        }

        private static void OnCircleRadiusPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Waiting waiting = d as Waiting;
            if (waiting == null || waiting._canvas == null)
            {
                return;
            }

            waiting.UpdateCanvasSize();
        }

        #endregion

        #region Event Mehtods

        private void canvas_Unloaded(object sender, RoutedEventArgs e)
        {
            Stop();
        }

        private void canvas_Loaded(object sender, RoutedEventArgs e)
        {
            if (IsBusy)
            {
                Start();
            }
        }

        private void _animationTimer_Tick(object sender, EventArgs e)
        {
            UpdateFirstDotPositionIndex();
            for (int i = 0; i < _dotList.Count; i++)
            {
                SetPosition(_dotList[i], i);
            }
        }

        #endregion

        #region Private Methods

        private void UpdateCanvasSize()
        {
            if (_canvas == null)
            {
                return;
            }

            _canvas.Width = (DotRadius + CircleRadius) * 2;
            _canvas.Height = (DotRadius + CircleRadius) * 2;
        }

        private void InitDots()
        {
            _dotList = new List<Shape>();
            double opacityStep = 1.0 / DotCount;
            for (int i = 0; i < DotCount; i++)
            {
                Ellipse ellipse = new Ellipse();
                ellipse.Name = "Dot" + i.ToString();
                ellipse.Width = DotRadius * 2;
                ellipse.Height = DotRadius * 2;
                ellipse.Stretch = Stretch.Fill;
                ellipse.Fill = DotForeground;
                ellipse.Opacity = 1 - i * opacityStep;

                _dotList.Add(ellipse);
                _canvas.Children.Add(ellipse);
            }
        }

        private void InitRegionPositionFactors()
        {
            _sinValueArray = new double[RegionCount];
            _cosValueArray = new double[RegionCount];

            _sinValueArray[0] = 0;
            _cosValueArray[0] = 1;

            int halfRegionCount = RegionCount / 2;
            double baseAngleWeight = 0.2;
            double baseAngle = (Math.PI * baseAngleWeight) / halfRegionCount;
            double unitAngle = Math.PI * (1 - baseAngleWeight) / (halfRegionCount * (1 + halfRegionCount) / 2);
            for (int i = 1; i <= halfRegionCount; i++)
            {
                double angle = i * (1 + i) / 2 * unitAngle + baseAngle * i;
                _sinValueArray[i] = Math.Sin(angle);
                _sinValueArray[RegionCount - i] = Math.Sin(Math.PI * 2 - angle);

                _cosValueArray[i] = Math.Cos(angle);
                _cosValueArray[RegionCount - i] = Math.Cos(Math.PI * 2 - angle);
            }
        }

        private void InitAnimationTimer()
        {
            _animationTimer = new DispatcherTimer(DispatcherPriority.Normal, Dispatcher);
            _animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 63);
            _animationTimer.Tick += _animationTimer_Tick;
        }

        private void Start()
        {
            if (_animationTimer != null)
            {
                _animationTimer.Start();
            }
        }

        private void UpdateFirstDotPositionIndex()
        {
            _firstDotPositionIndex++;
            if (_firstDotPositionIndex > RegionCount - 1)
            {
                _firstDotPositionIndex = 0;
            }
        }

        private void Stop()
        {
            if (_animationTimer != null)
            {
                _animationTimer.Stop();
            }
        }

        private int GetDotPositionIndex(int dotIndex)
        {
            int positionIndex = (_firstDotPositionIndex - dotIndex + RegionCount) % (RegionCount);
            return positionIndex;
        }

        private void SetPosition(UIElement element, int dotIndex)
        {
            int positionIndex = GetDotPositionIndex(dotIndex);
            double xValue = CircleRadius + CircleRadius * _sinValueArray[positionIndex];
            double yValue = (CircleRadius) * (1 - _cosValueArray[positionIndex]);

            element.SetValue(Canvas.LeftProperty, xValue);
            element.SetValue(Canvas.TopProperty, yValue);
        }

        #endregion
    }
}
